TCA が 1.0 になって対応したこと

いきなり 1.0 にアップデートせずに、まずは 0.58.0 で移行作業を行うのがおすすめ。
(参考: TCA migration questions and some experiences from 0.54 to 1.0 · pointfreeco/swift-composable-architecture · Discussion #2345 · GitHub)

0.58.0 であれば Deprecations は Warning として教えてくれるので、移行しやすい。
いきなり 1.0 にアップデートしてしまうとコンパイルができないためにすべての場所を教えてくれるわけじゃないし、関係ない別のエラーが出たりする。。

ReducerProtocol -> Reducer

置換で一括対応でビルドすれば特に漏れなく対応できた。

// Before
struct Fetaure: ReducerProtocol { /* ... */ }
var body: some ReducerProtocolOf<Self> { /* ... */ }

// After
struct Fetaure: Reducer { /* ... */ }
var body: some ReducerOf<Self> { /* ... */ }

EffectTask -> Effect

一部 EffectTask が残ってたので Effect に置き換え。
(.fireAndForget とか久々に目にした)

// Before
return .fireAndForget {
  doSomething()
}
return .task {
  let something = await getSomething()
  return .somethingResponse(something)
}

// After
return .run { _ in
  doSomething()
}
return .run { send in
  let something = await getSomething()
	await send(.somethingResponse(something))
}

store.stateless をやめる

Action を send するためだけで状態が変わらないボタン等で Store.stateless を利用していたが、 WWDC 2023 で発表された Observation に備えて Store.send(_:) が追加され、これに合わせて Store.stateless は削除された。

より詳しい Discussion としては RFC: Add `Store.send` and `Store.withState` · pointfreeco/swift-composable-architecture · Discussion #2223 · GitHub を参考。

Store.send(_:) のドキュメントを見ると、 ViewStore が存在する場合はそちらの send が望ましいとのこと [1] なので、すべての send を置き換える必要はない。

If a view store is available, prefer send(_:).

// Before
WithViewStore(store.stateless) { viewStore in
  Button {
    viewStore.send(.someAction)
  } label: {
    Text("Some button")
  }
}

// After
Button {
	store.send(.someAction)
} label: {
	Text("Some button")
}

Store の Reducer は builder block 使う

Add `Store.init` that takes reducer builder by stephencelis · Pull Request #2087 · pointfreeco/swift-composable-architecture · GitHub で導入されていて、 builder を使わない init は soft-deprecation になっていたが、削除された。
個人的には ReducerBuilder の中身みたいな書き味になって結構気に入っている。

var body: some ReducerOf<Feature> {
  Scope(state: \.child, action: /Action.child) {
    Child()
  }

  Reduce { /* ... */ }
}

変更としては以下の形。 Trailing closure の利用有無で 2 パターンに別れた。

// Bofore
ExampleView(
  store: .init(
    initialState: .init(),
    reducer: Example()
  )
)

// After (変更が最小)
ExampleView(
  store: .init(
    initialState: .init(),
    reducer: { Example() }
  )
)
// After (trailing closure を使う)
ExampleView(
  store: .init(initialState: .init()) {
    Example()
  }
)

WithViewStore(_:) -> WithViewStore(_:observe:)

TCA をよくわかってないときの実装が残っていた。 (今もよくわかってない)
WithViewStore(store)WithViewStore(store, observe: { $0 }) と同じなので、末端の View や小さい State 以外では避けて、適切なスコープだけを監視するようにして、パフォーマンスに気を使うようにしたい。

// Before
WithViewStore(store) { viewStore in /* ... */ }

// After
WithViewStore(store, observe: \.child) { viewStore in /* ... */ }

// After (以下のように State すべて監視するときは末端に近い View もしくは State が小さい時だけにする。)
WithViewStore(store, observe: { $0 }) { viewStore in /* ... */ }

alert(_:dismiss:) の廃止

alert(_:dismiss:) - ComposableArchitecture Documentation が削除された。
PresentationState を使った実装に変更した。

Effect.send を適切に使う

(1.0 はあまり関係ないがこの機に一緒にやったのでメモ)
.send の存在を知らなかったのか、後から Delegate action を導入したのかは覚えてないが、 Effect.run の中でただ send だけしているものがあったので Delegate action については [2] Effect.send を使うようにした。

We do not recommend using Effect.send to share logic. Instead, limit usage to child-parent communication, where a child may want to emit a “delegate” action for a parent to listen to.

inout state は Concurrency でキャプチャできないので、キャプチャリストを使って無理やり Delegate action を実行していた実装もあったので、合わせて修正した。

// Before
return .run { send in await send(.delegate(.something)) }
return .run { [childState = state.child] send in
  await send(.delegate(.delegationWithParameter(childState)))
}

// After
return .send(.delegate(.something))
return .send(.delegate(.delegationWithParameter(state.child)))

疑問が残るところ

Preview の中で Store を初期化する際に、 initialState のパラメータとして空配列を渡したいときに何故か [] だとコンパイラーになってしまった。
エラーの内容としては "Cannot convert value of type [Any] to expected argument type [Item]" のような感じ。
型推論が効いてない感じなので、とりあえず型を明示して [Item]() のように初期化を行った。

// Before
struct ExampleView_Previews: PreviewProvider {
    static var previews: some View {
        Example(
            store: .init(
                initialState: .init(items: []),
                reducer: Example()
            )
        )
    }
}

// After
struct ExampleView_Previews: PreviewProvider {
    static var previews: some View {
        Example(
            store: .init(
                initialState: .init(items: [Item]()), // ここ
                reducer: { Example() }
            )
        )
    }
}

swift-composable-architecture


  1. Store.send(_:) - ComposableArchitecture Documentation ↩︎

  2. Effect.send(_:) - ComposableArchitecture Documentation ↩︎